using System;
using System.Collections.Generic;
using System.Data.Linq;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Magiq.Support;
using Magiq.Support.DynamicClasses;
using Magiq.Update;

namespace Magiq.Sql {
  public abstract class DynamicSelectQueryExecutor : QueryExecutor {
    private readonly IEnumerable<IPropertyValueAssignment> assignments;
    private readonly List<string> calculatedColumns = new List<string>();
    private readonly ClassFactory classFactory = ClassFactory.Instance;
    private readonly List<string> constantColumns = new List<string>();
    private readonly List<object> constants = new List<object>();
    private readonly List<string> constantSqlValues = new List<string>();

    private readonly Dictionary<IPropertyValueAssignment, List<string>> queryDynamicClassMapping =
      new Dictionary<IPropertyValueAssignment, List<string>>();

    private IEnumerable<string> calculatedSqlValues;

    protected DynamicSelectQueryExecutor(DataContext dataContext, IQueryable where,
                                         IEnumerable<IPropertyValueAssignment> assignments, bool addTypeReference)
      : base(dataContext, where, addTypeReference) {
      Bindings = new List<MemberBinding>();
      this.assignments = assignments;
    }

    public string[] BaseSelectedValues { get; private set; }
    protected Type DynamicClass { get; private set; }
    protected IList<MemberBinding> Bindings { get; private set; }


    protected ParameterExpression Parameter { get; private set; }
    public string[] Columns { get; private set; }


    protected override void Init(Type elementType) {
      base.Init(elementType);
      Parameter = Expression.Parameter(Where.ElementType, "x");

      BuildDynamicClass();
      OnDynamicClassBuilt();
      ProcessPropertySets();
      ProcessDynamicSelect();
      ProcessConstantValues();

      Columns = calculatedColumns.Union(constantColumns).ToArray();
      BaseSelectedValues = calculatedSqlValues.Union(constantSqlValues).ToArray();
    }

    protected virtual void OnDynamicClassBuilt() {}

    protected virtual List<DynamicProperty> CreateDynamicProperties() {
      var properties = new List<DynamicProperty>();
      foreach (var assignment in assignments) {
        var propertyTypeMapping = new TableMapping(assignment.PropertyType, DataContext);

        var list = new List<string>();
        if (propertyTypeMapping.IsEntity) {
          foreach (var id in propertyTypeMapping.IdentityMembers) {
            var name = assignment.PropertyName + "_" + id.Name;
            properties.Add(new DynamicProperty(name, id.Type));
            list.Add(name);
          }
        }
        else {
          properties.Add(new DynamicProperty(assignment.PropertyName, assignment.ValueType));
          list.Add(assignment.PropertyName);
        }
        queryDynamicClassMapping.Add(assignment, list);
      }
      return properties;
    }

    private void BuildDynamicClass() {
      var properties = CreateDynamicProperties();
      DynamicClass = classFactory.GetDynamicClass(properties);
    }

    private void ProcessConstantValues() {
      foreach (var constant in constants) {
        if (constant == null) {
          constantSqlValues.Add("null");
        }
        else {
          var p = Command.CreateParameter();

          p.ParameterName = "@p" + Command.Parameters.Count;
          p.Value = constant;
          Command.Parameters.Add(p);
          constantSqlValues.Add(p.ParameterName);
        }
      }
    }

    private void ProcessDynamicSelect() {
      var newExpression = Expression.MemberInit(Expression.New(DynamicClass), Bindings);

      var lambda = Expression.Lambda(newExpression, Parameter);

      var selectQuery = Where.Provider
        .CreateQuery(Expression.Call(typeof (System.Linq.Queryable),
                                     "Select",
                                     new[] {Where.ElementType, lambda.Body.Type},
                                     Where.Expression,
                                     Expression.Quote(lambda)));

      SetCommand(DataContext.GetCommand(selectQuery));
      calculatedSqlValues = GetCalculatedSqlValues();
    }


    protected virtual IEnumerable<string> GetCalculatedSqlValues() {
      return calculatedColumns.IsEmpty()
               ? new List<string>()
               : Command.CommandText
                   .From("SELECT ")
                   .Until(" FROM ")
                   .Split(',')
                   .Select(x => x.Until(" AS ").Trim());
    }

    private void ProcessPropertySets() {
      foreach (var propertySet in assignments) {
        var columns = Mapping.ColumnsFor(propertySet.PropertyName);

        if (propertySet.ValueExpression.IsConstant()) {
          foreach (var constant in GetConstantsFrom(propertySet.ValueExpression))
            constants.Add(constant);

          constantColumns.AddRange(columns);
        }
        else {
          var dynamicProperties = queryDynamicClassMapping[propertySet];
          var expressions = GetSelectExpressionsFrom(propertySet.ValueExpression).ToArray();
          for (var i = 0; i < expressions.Length; i++) {
            var expression = expressions[i];
            var lambda = Expression.Lambda(expression, expression.GetParameter()).ChangeParameters(Parameter).Body;
            Bindings.Add(Expression.Bind(DynamicClass.GetProperty(dynamicProperties[i]), lambda));
          }

          calculatedColumns.AddRange(columns);
        }
      }
    }

    private IEnumerable<object> GetConstantsFrom(LambdaExpression expression) {
      var valueMapping = new TableMapping(expression.Body.Type, DataContext);
      var parameters = new object[expression.Parameters.Count];
      var value = expression.Compile().DynamicInvoke(parameters);

      if (valueMapping.IsEntity) {
        foreach (var id in valueMapping.IdentityMembers) {
          if (Equals(value, null))
            yield return null;
          else
            yield return value.Property(id.Name);
        }
      }
      else {
        yield return value;
      }
    }

    private IEnumerable<Expression> GetSelectExpressionsFrom(LambdaExpression expression) {
      var valueMapping = new TableMapping(expression.Body.Type, DataContext);

      if (valueMapping.IsEntity) {
        foreach (var id in valueMapping.IdentityMembers) {
          var pi = (PropertyInfo) id.Member;
          var property = Expression.Property(expression.Body, pi);
          yield return property;
        }
      }
      else {
        yield return expression.Body;
      }
    }
  }
}